// es6 -- main
// 作者 王欢
// 开始日期 2019-12-20
// ### 主要包括
// 表单验证集合
// 深度载入函数
// 手机验证码处理
// canvas验证码绘制
const { $copy, $toClass } = require("./common");
// 正则验证字典
const $regExp = {
    phone: /^1[3456789]\d{9}$/,
    email: /^([A-Za-z0-9_\-.])+@([A-Za-z0-9_\-.])+\.([A-Za-z]{2,4})$/,
    pass: /^[A-Za-z0-9]{6,20}$/,
    realName: /^[\u4E00-\u9FA5\uf900-\ufa2d·s]{2,20}$/,
    ID: /^[1-9][0-9]{5}(19|20)[0-9]{2}((01|03|05|07|08|10|12)(0[1-9]|[1-2][0-9]|31)|(04|06|09|11)(0[1-9]|[1-2][0-9]|30)|02(0[1-9]|[1-2][0-9]))[0-9]{3}([0-9]|x|X)$/
}


// 仿照vue指令  v-model
// 使用方法  绑定DOM
// 也可以主动使用
/* page({
    data:{
        person:{
            name:"wanghuan",
            age:[0,1,2]
        }
    }
}) */
//绑定 input事件
/* <input type="text" 
       bindinput = "$model" 
       data-model = "person.name" || "person.age[0]"
   ></input > */

//绑定点击或其他事件  需要指定 value
/* <view
       bindtap = "$model" 
       data-model = "person.name" || "person.age[0]"
       data-value = "123" || "{{ true || 123 || undefined }}"
   ></view > */

//同时写入对个对象
/* <input type="text" 
       bindinput = "$model" 
       data-model = "person.name,person.age[0]" 
   ></input > */

// 主动调用
// 回调函数success在  setData之后的  nextTick后调用
/* $model(undefined,{
    "person.name": 'hellow',
    "person.age[0]" : 18 
    // 回调函数
},function(){

}); */
/* 
    2021/3/29 新增特性  -- 写入的value可以为一个 函数  
    函数的第一个参数  为  即将正确写入的 key 之前的值 
    函数的this指向  为  调用$model的对象
    示例如下
    $model(undefined,{
        "age":function( ageOldVal ){
            console.log( ageoldVal + 1 );
            return ageOldVal + 1;
        }
    });
    {
        data:{
            age: 1
        }
    }
    正确执行结果应为:
         age === 2
*/
/* 
    $model(event:EventObject[,ops:any,success:any]);
    $model(event:void,opts:Object[,success:Function]);
*/
function $model(event, opts, success) {
    let page = this;
    let pageData = page;
    let AssOpts;
    // 进行参数合并  两者选其一   
    // event优先  
    if (event) {
        let eventData = event.currentTarget.dataset;
        let edModel = eventData.model;
        if (edModel) {
            AssOpts = {};
            let modelVal;
            // 判断 data-value  属性是否存在
            // 当  ( data-value ="" ) 时也是需要载入
            if (!eventData.hasOwnProperty("value")) {
                modelVal = event.detail.value;
            } else {
                modelVal = eventData.value;
            }
            AssOpts[edModel] = modelVal;
        } else {
            throw new Error("the event model is must be required");
        }
    } else {
        if (opts && $toClass(opts) == "Object") {
            AssOpts = opts;
        } else {
            throw new Error("the opts is must be required and the type is Object");
        }
    }

    if (AssOpts) {
        // 保存需要写入的所有对象  
        // 减少 setData 合并额外没有变动属性  
        let modelAss = {};
        Object.keys(AssOpts).forEach(keyNode => {
            // 获取value
            let value = AssOpts[keyNode];
            // 2021/3/29 新增 value === function
            const valueIsFun = typeof value === "function";

            // 如果存在分割符  , 或者 | 转换为数组
            // 消除空格 
            let modelArr = $trimAll(keyNode).split(/[,|]/);
            // 对分割后的model进行循环写入
            modelArr.forEach(modelNode => {
                // 转换modelNode上的数组元素为统一分隔符
                let modelAga = modelNode.replace(/(\[)|(\])/gm, (match, p1, p2) => {
                    if (p1) {
                        return '.';
                    } else if (p2) {
                        return '';
                    } else {
                        return match;
                    }
                });
                //进行深度写入
                let modelLeve = modelAga.split(".");
                let copyData = pageData;
                for (let j = 0, l = modelLeve.length; j < l; j++) {
                    let iterKey = modelLeve[j];
                    if (iterKey) {
                        let iterObj = copyData[iterKey];
                        if (j == l - 1) {
                            // 如果value为一个函数  则把 之前的value 作为函数的第一个值
                            // 函数 this  指向调用 page对象
                            const finalModelVal = copyData[iterKey];
                            if (valueIsFun) {
                                copyData[iterKey] = value.call(page, finalModelVal);
                            } else {
                                copyData[iterKey] = value;
                            }
                            break;
                        } else {
                            copyData = iterObj;
                        }
                    } else {
                        throw new Error("the leve is not be Null");
                    }
                }
                let copyKey = modelLeve[0];
                modelAss[copyKey] = pageData[copyKey];

            });
        });
        // 先进行视图渲染
        //page.setData(modelAss);
        if (!event && opts) {
            // 成功回调函数
            if (success && typeof success == "function") {
                uni.nextTick(function () {
                    success(modelAss);
                });
            }
        }

    } else {
        console.warn("missing necessary parameters");
    }

}

/* 
    setData 添加小程序 api 
    当项目从小程序迁移uniapp的情况下  非常有用
*/
function setData(params) {
    if (params) {
        const vm = this;
        const paramsType = $toClass(params);
        if (paramsType === "Object") {
            Object.assign(vm, params);
            $model.call(vm, undefined, params);
        } else {
            throw TypeError(`setData Params type must be Object,now type is [${paramsType}]`);
        }
    } else {
        console.warn("ignore: setData params is undefined");
    }
}


// 发送验证码  支持promise
// 参数字典
/* opts = {
    // 需要验证号码类型  手机号码 或者 邮箱
    codeType: "phone" || "email",
    // 验证码关联号码value #required
    codeValue: "",          
    // 发送验证码 model
    // 验证码迭代key  基于$model进行深度载入 #required
    modelKey: "codeOptions", 
    // 验证码参数保存对象
    // 计时最大时间
    maxTime: 60,
    // 发送状态  默认 未发送状态 #required
    sendState: false,       
    // 是否发送过验证码 #required
    sendAuto: false,        
    // 默认按钮显示文本 #required
    startText: "获取验证码", 
    // 发送过之后显示文本
    endText: "重新发送" ,
    // 发送成功后 接受的验证码 value 忽略   可以在回调中进行自由赋值
    sendValue:"",
    // 请求参数路径 #required  url默认拼接全局api(字符串类型)  如添加非全局api或者其他ajax参数可以改为对象类型(url路径必须为完整路径)
    url:"",
    // 请求参数
    data:"",
    // 请求方式 默认 post
    methods: "POST",
}; */
// 方法依赖 $model   进行深度载入
//       --  $ajax  进行网络请求
// 示例一
/* 
//  
export default {
    data(){
        return {
            sendCodeInfo:{
                codeValue:"",  // 必填  号码载入
                modelKey: "sendCodeInfo", // 必填  验证码对象载入的路径
                startText: "获取验证码", // 发送验证码初始化文本
                sendState: false,
                sendAuto: false,
                method: "POST", // 默认请求方式
                url:"http path", // 必填  验证码请求路径
                data:{ // http  携带的参数
                    phone: "17315613303" 
                }
            }
        }
    },
    methods:{
        $model,
        $sendCode,
        sendVerify() {
            const { sendCodeInfo } = this;
            this.$sendCode( sendCodeInfo )
            .then(res => {
                console.log(res);
            })
            .catch(err => {
                console.log(err);
            })

        }
    }
}
<view
    catchtap="sendVerify"
>{{ sendCodeInfo.startText }}</view>

*/
/* 
    $sendCode(opts:Object[,suc:Function]);
*/
function $sendCode(opts, suc) {
    return new Promise((resolve, reject) => {
        let defaultOpts = {
            codeType: "phone",
            // codeValue:"",
            modelKey: "",
            startText: "获取验证码",
            // url:"",
            // data:{},
            // sendState: false,
            // sendAuto: false,
            maxTime: 60,
            sendValue: "",
            endText: "重新获取",
            method: "POST",
        };

        if (opts) {
            let optsType = $toClass(opts);
            if (optsType == "Object") {
                $copy(defaultOpts, opts);
                // 如果处在发送状态则阻止下面动作  并发出一个警告
                if (defaultOpts.sendState) {
                    console.warn("It is being sent, please hold on");
                    return false;
                }
                let { codeType, codeValue } = defaultOpts;
                let codeText = codeType == "phone" ? "手机" : "邮箱";
                // console.log( defaultOpts )
                // 判断号码存在
                if (codeValue) {
                    // 根据正则字典获取 对应正则
                    let codeReg = $regExp[codeType];
                    // 判断 codeType 是否正确关键字
                    if (codeReg) {
                        if (codeReg.test(codeValue)) {
                            // 获取 page对象
                            const page = this;
                            const pageData = page;
                            // 获取model参数 与 key
                            let { modelKey } = defaultOpts;
                            // 判断modelkey是否存在  为深度载入拼接key
                            let symbolKey = modelKey + (modelKey ? "." : "");
                            // 进行网络请求
                            let { url, data, method = "POST" } = defaultOpts;
                            let requestAss = { method };
                            if (url) {
                                let urlType = $toClass(url);
                                if (urlType == "String") {
                                    $copy(requestAss, {
                                        url,
                                        data
                                    });
                                } else if (urlType == "Object") {
                                    $copy(requestAss, url);
                                } else {
                                    throw new Error("the url type is must be sting or object");
                                }
                            } else {
                                throw new Error("the url params is not defined")
                            }
                            $request(requestAss, data)
                                .then(res => {
                                    // 请求成功后把  data传给回调函数
                                    // 同时开始倒计时
                                    // setTimeout(function () {
                                    /* let res = {
                                        code: "1234",
                                    }; */
                                    let { maxTime, endText } = defaultOpts;
                                    let interTimer = setInterval(function () {
                                        page.$model(false, {
                                            [symbolKey + "startText"]: `剩余 ${maxTime - 1}s`
                                        });
                                        if (maxTime) {
                                            maxTime--;
                                        } else {
                                            clearInterval(interTimer);
                                            page.$model(false, {
                                                [symbolKey + "startText"]: endText || "重新发送",
                                                [symbolKey + "sendState"]: false,
                                            });
                                        }
                                    }, 1000);
                                    page.$model(false, {
                                        [symbolKey + "startText"]: `剩余 ${maxTime}s`,
                                        [symbolKey + "sendAuto"]: true,
                                        [symbolKey + "sendState"]: true,
                                    });
                                    // 执行回调函数操作返回的数据及其他操作
                                    suc && typeof suc == "function" && suc(res);
                                    $log("发送成功！");
                                    resolve(res);
                                    // }, 1000)
                                }).catch(err => {
                                    console.log(err);
                                    $log("接口请求失败");
                                    reject(err);

                                });
                        } else {
                            $log(`${codeText}号码格式错误`);
                        }

                    } else {
                        throw new Error("the codeType must be 'phone' or 'email'");
                    }
                } else {
                    $log(`请输入${codeText}号码`);
                }
            } else {
                throw new TypeError("the opts type must be Object");
            }
        } else {
            throw new ReferenceError("the opts must be required");
        }
    });
}




// 绘制验证码
// 绘制尺寸  支持  px 与 rpx
// 参数字典
/* opts={
    // 指定绘制类型
    // 默认全部  包括 数字[0-9] 英文 [a-zA-Z]  
    type, // String -> "all" | "number" |  "string"
    // 指定绘制 value 
    value, // String length == 4
    width, // Number || String #requird -> 100 |  "100px" | "100rpx"  
    height, // 同上
    // 绘制canvas id
    id, // String #requird
}   
    函数返回一个 value值  作为判断
*/
// 示例
/* 
    page({
        drawInfo:{
            id:"canvas",
            width: "100px",
            height: "50px",
            type:"number"
            // value: "1205"
        },
        drawCav(){
            this.$drawCode(this.data.drawInfo, (res) => {
                this.setData({
                    drawVal: res
                })
            });
        },
        onLoad: function (options) {
            this.drawCav();
        },
    })

    <view>
        <canvas
            style="width:{{ drawInfo.width }};height:{{ drawInfo.height }};"  
            canvas-id="{{ drawInfo.id }}" 
            bindtap="drawCav"
        ></canvas>
    </view>
    <view>{{ drawVal }}</view>

*/

// 返回一个随机数
/* 
   _randomNum(min:Number, max:Number);  
*/
function _randomNum(min, max) {
    return Math.floor(Math.random() * (max - min) + min);
}
// 返回一个随机色
/* 
    _randomColor(min:Number, max:Number);  
*/
function _randomColor(min, max) {
    let r = _randomNum(min, max),
        g = _randomNum(min, max),
        b = _randomNum(min, max);
    return `rgb(${r},${g},${b})`;
}
/* 
    返回一个绘制字符串
    可以在 suc 第一个参数拿到 绘制字符串 执行回调函数
    $drawCode(opts:Object[, suc:Function]);
*/
function $drawCode(opts, suc) {
    // 参数合并 类型判断
    let defaultOpts = {
        type: "all",
    };
    if (opts && $toClass(opts) == "Object") {
        // 获取 id 宽高
        let { id, width, height } = $copy(defaultOpts, opts);
        if (id && width && height) {
            let canVal;
            let drawData;
            let ctx = uni.createCanvasContext(id);
            // 如果value参数没有  最常见的情况
            let { type, value } = defaultOpts;
            // 判断value 转换一组渲染字符串数组
            let filterData = function (str, rdm = true) {
                let data = [];
                if (rdm) {
                    let len = str.length - 1;
                    for (let i = 0; i < 4; i++) {
                        let key;
                        data.push(
                            str[_randomNum(0, len)]
                        );
                    }
                } else {
                    data = Array.prototype.slice.call(str);
                }
                return data;
            };
            // 根据类型获取绘制数组
            // 没有指定value存在的情况
            if (!value) {
                let codeData = [
                    'abcefghjklmnpqrstwxyABCEFGHJKLMNPQRSTWXY',
                    '0123456789'
                ];
                let sliceData;
                if (type == "all") {
                    sliceData = codeData.join("");
                } else if (type == "number") {
                    sliceData = codeData[1];
                } else if (type == "string") {
                    sliceData = codeData[0];
                } else {
                    throw new Error("the type is must be all,number or string")
                }
                drawData = filterData(sliceData);
                // 有指定value
            } else {
                if (value && typeof value == "string" && value.length == 4) {
                    drawData = filterData(value, false);
                } else {
                    throw new Error("the value params type must be string and length == 4");
                }
            }
            // 判断 width 或者 height是否存在rpx单位
            // 有则根据 缩放比例计算实际宽高

            let scale = 1;
            if ((width + height).search("rpx") != -1) {
                let { windowWidth } = uni.getSystemInfoSync();
                scale = windowWidth / 750;
            }
            // 暂且只支持整数
            // 根据缩放比例重新计算宽高尺寸
            let scaleSize = function (size) {
                let num = parseInt(size);
                if (size.search("rpx") != -1) {
                    return num * scale;
                }
                return num;
            };
            let drawWidth = scaleSize(width);
            let drawHeight = scaleSize(height);
            /**绘制背景色**/
            //颜色若太深可能导致看不清
            ctx.fillStyle = _randomColor(180, 240);
            ctx.fillRect(0, 0, drawWidth, drawHeight)
            /**绘制文字**/
            for (let i = 0; i < 4; i++) {
                let ti = drawData[i],
                    x = drawWidth / 4 / 10 + i * (drawWidth / 4),
                    y = _randomNum(drawHeight - drawHeight / 5, drawHeight - drawHeight / 10),
                    deg = _randomNum(-20, 20);
                ctx.fillStyle = _randomColor(50, 120); //随机生成字体颜色
                ctx.font = _randomNum(20, drawHeight) + 'px SimHei'; //随机生成字体大小
                //修改坐标原点和旋转角度
                let transX = i ? x : 0;
                ctx.translate(transX, y);
                ctx.rotate(deg * Math.PI / 180);
                ctx.fillText(ti, drawWidth / 4 / 4, 0);

                //恢复坐标原点和旋转角度
                ctx.rotate(-deg * Math.PI / 180);
                ctx.translate(-transX, -y);
                // /**绘制干扰线**/
                ctx.strokeStyle = _randomColor(120, 230);
                ctx.beginPath();
                ctx.moveTo(_randomNum(0, drawWidth), _randomNum(0, drawHeight));
                ctx.lineTo(_randomNum(0, drawWidth), _randomNum(0, drawHeight));
                ctx.stroke();
                canVal += ti;
            }
            // /**绘制干扰点**/
            for (let j = 0; j < 10; j++) {
                ctx.fillStyle = _randomColor(0, 255);
                ctx.beginPath();
                ctx.arc(_randomNum(0, drawWidth), _randomNum(0, drawHeight), 1, 0, 2 * Math.PI);
                ctx.fill();
            }
            // 绘制完成
            ctx.draw();
            // 返回 绘制字符串
            let drawStr = drawData.join("");
            suc && typeof suc == "function" && suc(drawStr);
            return drawStr;

        } else {
            throw new ReferenceError("missing necessary parameters");
        }
    } else {
        throw new Error("the opts must be required or type is Object");
    }
}
/* 
    数字格式化
    $checkTime(i:Number|String);
*/
function $checkTime(i) {
    i = +i;
    return i < 10 && i >= 0 ? '0' + i : i;
};

/* 
    
    // 计算剩余时间 
    // 如果有剩余时间  则返回一个数组 分别是  [天数,小时,分钟,秒数] 
    $leftDate(opts:String|DateObject[,clearDays:Boolean]);
    返回值
       -- 如果传入的时间  大于等于当前的时间
       返回{
            d // 天数
            ,h // 小时
            ,m // 分钟
            ,s // 秒
            ,code:[ d,h,m,s ] 
            //如果第二个参数为 true  且时间差为24小时以内 返回 
            [h,m,s]
       }
     -- 小于当前时间  返回  false      
*/

function $leftDate(dateParams, clearDays = false) {
    if (dateParams !== undefined) {
        const paramsType = typeof dateParams;
        let endDate = null;
        if (paramsType === "string") {
            endDate = new Date(dateParams);
        } else if (paramsType === "object") {
            endDate = dateParams;
        } else {
            throw TypeError("the params type must be String or DateObject");
        }
        const leftTimes = endDate.getTime() - new Date().getTime();
        if (leftTimes >= 0) {
            const leftDays = $checkTime(parseInt(leftTimes / 1000 / 60 / 60 / 24));
            const leftHours = $checkTime(parseInt(leftTimes / 1000 / 60 / 60 % 24));
            const leftMinutes = $checkTime(parseInt(leftTimes / 1000 / 60 % 60));
            const leftSeconds = $checkTime(parseInt(leftTimes / 1000 % 60));
            const code = [leftDays, leftHours, leftMinutes, leftSeconds];
            if (clearDays && Number(leftDays) === 0) {
                code.splice(0, 1);
            }
            return {
                d: leftDays
                , h: leftHours
                , m: leftMinutes
                , s: leftSeconds
                , code
            }
        } else {
            return false;
        }
    } else {
        throw new ReferenceError("the params must be required");
    }
}


/* 转换时间戳
    返回
    {
       year:  Number
      ,month: String || Number
      ,date: String || Number
      ,hours: String || Number
      ,minutes: String || Number
      ,seconds: String || Number
      ,format: String -> "2019-10-20 03:20:50"
      ,format_date: String -> "2019-10-20"
      ,format_time: String -> "03:20:50"
      ,_date: Date Object
      ,_timestamp: Number
    }
*/
function $formatDate(dateParams) {
    const dateType = $toClass(dateParams);

    let dateObj;
    if (dateType === "Number"
        || dateType === "String"
    ) {
        dateObj = new Date(dateParams);
    } else if (dateType === "Date") {
        dateObj = dateParams;
    } else {
        throw TypeError("the dateParams type must be [String,Number,Date]");
    }
    const year = $checkTime(dateObj.getFullYear());
    const month = $checkTime(dateObj.getMonth() + 1)
    const date = $checkTime(dateObj.getDate());
    const hours = $checkTime(dateObj.getHours());
    const minutes = $checkTime(dateObj.getMinutes());
    const seconds = $checkTime(dateObj.getSeconds());
    const format = `${year}-${month}-${date} ${hours}:${minutes}:${seconds}`;
    const [format_date, format_time] = format.split(" ");
    return {
        year
        , month
        , date
        , hours
        , minutes
        , seconds
        , format
        , format_date
        , format_time
        , _date: dateObj
        , _timestamp: dateObj.getTime()
    };
}
/* 
    时间对比格式化
*/
function $getDateDiff(dateTime) {

    const now = new Date().getTime();
    const dateTimeStamp = new Date(dateTime).getTime();

    let result = "";
    const minute = 1000 * 60;
    const hour = minute * 60;
    const day = hour * 24;
    // const halfamonth = day * 15;
    const month = day * 30;
    const year = day * 365;

    const diffValue = now - dateTimeStamp;

    if (diffValue < 0) {
        return "刚刚";
    }
    const monthEnd = diffValue / month;
    const weekEnd = diffValue / (7 * day);
    const dayEnd = diffValue / day;
    const hourEnd = diffValue / hour;
    const minEnd = diffValue / minute;
    const yearEnd = diffValue / year;
    if (yearEnd >= 1) {
        result = dateTime;
    } else if (monthEnd >= 1) {
        // result = "" + parseInt(monthEnd) + "月前";
        result = dateTime;
    } else if (weekEnd >= 1) {
        result = "" + parseInt(weekEnd) + "周前";
    } else if (dayEnd >= 1) {
        result = "" + parseInt(dayEnd) + "天前";
    } else if (hourEnd >= 1) {
        result = "" + parseInt(hourEnd) + "小时前";
    } else if (minEnd >= 1) {
        result = "" + parseInt(minEnd) + "分钟前";
    } else {
        result = "刚刚";
    }
    return result;
};



module.exports = {
    /* 正则字典 */
    $regExp
    , $model
    , setData
    , $sendCode
    , $drawCode
    , $checkTime
    , $leftDate
    , $formatDate
    , $getDateDiff
}

